// --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } }

Projects

Strust2025

Cobol modernization performance metric for development of Ai models to convert COBOL to java. Strust.us

View PDF
Time Series Prediction2024

Our project enhances the LSTM-FCN architecture for time series classification, incorporating modifications like SE blocks, achieving improved performance across datasets compared to replication models.

View PDF
Pico Size Chatbot2024

Built a chatbot for you to ask about my experience in different projects and jobs. I built this from scratch (similar to GPT 2 paper) on AWS. Trained on school cluster.

MealMapper - Columbia Startup Lab2024

Engineered an AI-powered meal tracking application, implementing computer vision models (PyTorch, YOLOv8) for automated food recognition and nutritional estimation from images. Deployed the deep learning system on GCP for use by sports team nutritionists.

Senior Design 2 - UCSB 2024

I developed a process design and conducted an analysis for a carbon-negative dimethyl carbonate (DMC) plant, including the evaluation of equipment and economic factors. After optimizing the design, I found profitability improvements, by leveraging carbon credits in the European market.

View PDF
Senior Design 1 - UCSB 2023

I preformed a techno-economic analysis for a profitable and carbon-neutral steam ethane cracker. The optimization covered distillation, reactor, and heat exchanger networks, with optimized conditions yielding favorable profitability metrics.

View PDF
SeePrint - UCSB New Venture Incubator2022

Developed and deployed neural network systems on AWS to optimize OSIsoft PI process control, achieving up to 20% efficiency gains. Implemented data pipelines integrating plant sensor data for cloud-based analytics and model training/validation.

EarliBird - UCSB New Venture Incubator2021

Developed a TensorFlow-based natural language processing system, including custom tokenization, to analyze transaction patterns for optimizing retail store item placement and supply chain logistics, significantly reducing operational costs compared to cloud alternatives.

George Weale