imported files from google gemini ai BUILD env

This commit is contained in:
2025-11-10 08:28:40 -08:00
parent da3acd3a8a
commit f6ce97019e
114 changed files with 10955 additions and 2 deletions

206
components/ShoppingList.tsx Normal file
View File

@@ -0,0 +1,206 @@
import React, { useState, useMemo, useCallback } from 'react';
import { Session } from '@supabase/supabase-js';
import type { ShoppingList, ShoppingListItem } from '../types';
import { UserIcon } from './icons/UserIcon';
import { ListBulletIcon } from './icons/ListBulletIcon';
import { LoadingSpinner } from './LoadingSpinner';
import { TrashIcon } from './icons/TrashIcon';
import { SpeakerWaveIcon } from './icons/SpeakerWaveIcon';
import { generateSpeechFromText } from '../services/geminiService';
import { decode, decodeAudioData } from '../utils/audioUtils';
interface ShoppingListComponentProps {
session: Session | null;
lists: ShoppingList[];
activeListId: number | null;
onSelectList: (listId: number) => void;
onCreateList: (name: string) => Promise<void>;
onDeleteList: (listId: number) => Promise<void>;
onAddItem: (item: { customItemName: string }) => Promise<void>;
onUpdateItem: (itemId: number, updates: Partial<ShoppingListItem>) => Promise<void>;
onRemoveItem: (itemId: number) => Promise<void>;
}
export const ShoppingListComponent: React.FC<ShoppingListComponentProps> = ({ session, lists, activeListId, onSelectList, onCreateList, onDeleteList, onAddItem, onUpdateItem, onRemoveItem }) => {
const [isCreatingList, setIsCreatingList] = useState(false);
const [customItemName, setCustomItemName] = useState('');
const [isAddingCustom, setIsAddingCustom] = useState(false);
const [isReadingAloud, setIsReadingAloud] = useState(false);
const activeList = useMemo(() => lists.find(list => list.id === activeListId), [lists, activeListId]);
const { neededItems, purchasedItems } = useMemo(() => {
if (!activeList) return { neededItems: [], purchasedItems: [] };
const neededItems: ShoppingListItem[] = [];
const purchasedItems: ShoppingListItem[] = [];
activeList.items.forEach(item => {
if (item.is_purchased) {
purchasedItems.push(item);
} else {
neededItems.push(item);
}
});
return { neededItems, purchasedItems };
}, [activeList]);
const handleCreateList = async () => {
const name = prompt("Enter a name for your new shopping list:");
if (name && name.trim()) {
setIsCreatingList(true);
await onCreateList(name.trim());
setIsCreatingList(false);
}
};
const handleDeleteList = async () => {
if (activeList && window.confirm(`Are you sure you want to delete the "${activeList.name}" list? This cannot be undone.`)) {
await onDeleteList(activeList.id);
}
};
const handleAddCustomItem = async (e: React.FormEvent) => {
e.preventDefault();
if (!customItemName.trim()) return;
setIsAddingCustom(true);
await onAddItem({ customItemName: customItemName.trim() });
setCustomItemName('');
setIsAddingCustom(false);
};
const handleReadAloud = useCallback(async () => {
if (!activeList || neededItems.length === 0) return;
setIsReadingAloud(true);
try {
const listText = "Here is your shopping list: " + neededItems.map(item => item.custom_item_name || item.master_item?.name).join(', ');
const base64Audio = await generateSpeechFromText(listText);
// Play the audio
const audioContext = new (window.AudioContext)({ sampleRate: 24000 });
const audioBuffer = await decodeAudioData(decode(base64Audio), audioContext, 24000, 1);
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
} catch (e: any) {
console.error("Failed to read list aloud:", e);
alert(`Could not read list aloud: ${e.message}`);
} finally {
setIsReadingAloud(false);
}
}, [activeList, neededItems]);
if (!session) {
return (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 text-center">
<div className="flex flex-col items-center justify-center h-full min-h-[150px]">
<UserIcon className="w-10 h-10 text-gray-400 mb-3" />
<h4 className="font-semibold text-gray-700 dark:text-gray-300">Your Shopping Lists</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Please log in to manage your shopping lists.
</p>
</div>
</div>
);
}
return (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-bold text-gray-800 dark:text-white flex items-center">
<ListBulletIcon className="w-6 h-6 mr-2 text-brand-primary" />
Shopping List
</h3>
<button
onClick={handleReadAloud}
disabled={isReadingAloud || !activeList || neededItems.length === 0}
className="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700/50 text-gray-500 dark:text-gray-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Read list aloud"
>
{isReadingAloud ? <div className="w-5 h-5"><LoadingSpinner/></div> : <SpeakerWaveIcon className="w-5 h-5" />}
</button>
</div>
<div className="space-y-3 mb-4">
{lists.length > 0 && (
<select
value={activeListId || ''}
onChange={(e) => onSelectList(Number(e.target.value))}
className="block w-full pl-3 pr-8 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-brand-primary focus:border-brand-primary"
>
{lists.map(list => <option key={list.id} value={list.id}>{list.name}</option>)}
</select>
)}
<div className="flex space-x-2">
<button onClick={handleCreateList} disabled={isCreatingList} className="flex-1 text-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 font-semibold py-2 px-3 rounded-md transition-colors">
New List
</button>
<button onClick={handleDeleteList} disabled={!activeList} className="flex-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:text-red-300 font-semibold py-2 px-3 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
Delete List
</button>
</div>
</div>
{activeList ? (
<>
<form onSubmit={handleAddCustomItem} className="flex space-x-2 mb-4">
<input
type="text"
value={customItemName}
onChange={(e) => setCustomItemName(e.target.value)}
placeholder="Add a custom item..."
className="flex-grow block w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm sm:text-sm"
disabled={isAddingCustom}
/>
<button type="submit" disabled={isAddingCustom || !customItemName.trim()} className="bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2 px-3 rounded-lg flex items-center justify-center">
{isAddingCustom ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Add'}
</button>
</form>
<div className="space-y-2 max-h-80 overflow-y-auto">
{neededItems.length > 0 ? neededItems.map(item => (
<div key={item.id} className="group flex items-center space-x-2 text-sm">
<input
type="checkbox"
checked={item.is_purchased}
onChange={() => onUpdateItem(item.id, { is_purchased: !item.is_purchased })}
className="h-4 w-4 rounded border-gray-300 text-brand-primary focus:ring-brand-secondary"
/>
<span className="flex-grow text-gray-800 dark:text-gray-200">{item.custom_item_name || item.master_item?.name}</span>
<button onClick={() => onRemoveItem(item.id)} className="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-700 p-1">
<TrashIcon className="w-4 h-4"/>
</button>
</div>
)) : (
<p className="text-sm text-gray-500 text-center py-4">This list is empty.</p>
)}
{purchasedItems.length > 0 && (
<div className="pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
<h4 className="text-xs font-semibold text-gray-500 uppercase mb-2">Purchased</h4>
{purchasedItems.map(item => (
<div key={item.id} className="group flex items-center space-x-2 text-sm">
<input
type="checkbox"
checked={item.is_purchased}
onChange={() => onUpdateItem(item.id, { is_purchased: !item.is_purchased })}
className="h-4 w-4 rounded border-gray-300 text-brand-primary focus:ring-brand-secondary"
/>
<span className="flex-grow text-gray-500 dark:text-gray-400 line-through">{item.custom_item_name || item.master_item?.name}</span>
<button onClick={() => onRemoveItem(item.id)} className="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-700 p-1">
<TrashIcon className="w-4 h-4"/>
</button>
</div>
))}
</div>
)}
</div>
</>
) : (
<div className="text-center py-10">
<p className="text-gray-500">No shopping lists found. Create one to get started!</p>
</div>
)}
</div>
);
};