Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions beacon/src/components/ChoresView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StreakBadge } from './StreakBadge';
import { useChores } from '../hooks/useChores';
import { useFamily } from '../hooks/useFamily';
import { useSettings } from '../hooks/useSettings';
import { useTranslation } from '../i18n';

const CHORE_ICONS = ['🧹', '🍽️', '🐕', '🛏️', '📚', '🗑️', '👕', '🧺', '🪥', '🚿', '🧼', '💪'];

Expand Down Expand Up @@ -36,6 +37,7 @@ function UnassignedChoreCard({
onDelete,
currencySymbol = '$',
}: UnassignedChoreCardProps) {
const { t } = useTranslation();
const [showMemberPicker, setShowMemberPicker] = useState(false);
const [pickerAction, setPickerAction] = useState<'claim' | 'complete'>('claim');
const [confirmDelete, setConfirmDelete] = useState(false);
Expand Down Expand Up @@ -80,7 +82,7 @@ function UnassignedChoreCard({
setShowMemberPicker(true);
}}
>
Claim
{t('chores.claim')}
</button>
<button
type="button"
Expand All @@ -90,24 +92,24 @@ function UnassignedChoreCard({
setShowMemberPicker(true);
}}
>
Complete
{t('chores.complete')}
</button>
<button
type="button"
className="chore-card-action-btn"
onClick={onEdit}
aria-label={`Edit ${chore.name}`}
aria-label={t('chores.editChoreAria', { name: chore.name })}
>
<Pencil size={15} />
</button>
<button
type="button"
className="chore-card-action-btn chore-card-action-btn--danger"
onClick={handleDeleteClick}
aria-label={`Delete ${chore.name}`}
aria-label={t('chores.deleteChoreAria', { name: chore.name })}
>
{confirmDelete ? (
<span className="chore-card-action-confirm">Sure?</span>
<span className="chore-card-action-confirm">{t('common.sure')}</span>
) : (
<Trash2 size={15} />
)}
Expand All @@ -119,7 +121,7 @@ function UnassignedChoreCard({
<div className="modal modal--sm" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">
{pickerAction === 'claim' ? 'Who claims this?' : 'Who completed this?'}
{pickerAction === 'claim' ? t('chores.whoClaims') : t('chores.whoCompleted')}
</h2>
<button
type="button"
Expand Down Expand Up @@ -153,6 +155,7 @@ function UnassignedChoreCard({
}

export function ChoresView() {
const { t } = useTranslation();
const { members } = useFamily();
const { settings } = useSettings();
const {
Expand Down Expand Up @@ -265,28 +268,28 @@ export function ChoresView() {
return (
<div className="chores-view">
<header className="chores-view-header">
<h1 className="chores-view-title">Chores</h1>
<h1 className="chores-view-title">{t('chores.title')}</h1>
{members.length > 0 && (
<button
type="button"
className="btn btn--primary"
onClick={() => openAddForm()}
>
<Plus size={16} strokeWidth={2} />
Add Chore
{t('chores.addChore')}
</button>
)}
</header>

{members.length === 0 ? (
<div className="chores-empty">
Add family members first to assign chores.
{t('chores.addMembersFirst')}
</div>
) : (
<>
{unassignedChores.length > 0 && (
<section className="chores-unassigned-section">
<h2 className="chores-section-title">Open Chores</h2>
<h2 className="chores-section-title">{t('chores.openChores')}</h2>
<div className="chores-unassigned-list">
{unassignedChores.map((chore) => (
<UnassignedChoreCard
Expand Down Expand Up @@ -342,7 +345,7 @@ export function ChoresView() {

<div className="chores-member-col-list">
{memberChores.length === 0 ? (
<div className="chores-member-empty">No chores assigned</div>
<div className="chores-member-empty">{t('chores.noChoresAssigned')}</div>
) : (
<div className="chores-list">
{memberChores.map((chore) => (
Expand All @@ -368,7 +371,7 @@ export function ChoresView() {
onClick={() => openAddForm(member.id)}
>
<Plus size={14} strokeWidth={2} />
Add chore for {member.name}
{t('chores.addChoreFor', { name: member.name })}
</button>
</section>
))}
Expand All @@ -380,7 +383,7 @@ export function ChoresView() {
<div className="modal-overlay" onClick={handleCloseForm}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">{isEditing ? 'Edit Chore' : 'New Chore'}</h2>
<h2 className="modal-title">{isEditing ? t('chores.editChore') : t('chores.newChore')}</h2>
<button type="button" className="modal-close" onClick={handleCloseForm}>
{'\u00D7'}
</button>
Expand All @@ -389,7 +392,7 @@ export function ChoresView() {
<div className="modal-body chores-add-form">
{/* Icon picker */}
<div className="form-field">
<label className="form-label">Icon</label>
<label className="form-label">{t('chores.icon')}</label>
<div className="fm-avatar-grid">
{CHORE_ICONS.map((icon) => (
<button
Expand All @@ -405,19 +408,19 @@ export function ChoresView() {
</div>

<div className="form-field">
<label className="form-label">Name</label>
<label className="form-label">{t('chores.name')}</label>
<input
type="text"
className="form-input"
value={newChore.name}
onChange={(e) => setNewChore((f) => ({ ...f, name: e.target.value }))}
placeholder="e.g., Vacuum living room"
placeholder={t('chores.namePlaceholder')}
autoFocus
/>
</div>

<div className="form-field">
<label className="form-label">Value</label>
<label className="form-label">{t('chores.value')}</label>
<div className="chores-value-input">
<span className="chores-value-prefix">
{settings.currencySymbol === STAR_CURRENCY ? STAR_CURRENCY : '$'}
Expand Down Expand Up @@ -446,22 +449,22 @@ export function ChoresView() {
</div>

<div className="form-field">
<label className="form-label">Frequency</label>
<label className="form-label">{t('chores.frequency')}</label>
<select
className="form-select"
value={newChore.frequency}
onChange={(e) =>
setNewChore((f) => ({ ...f, frequency: e.target.value as Chore['frequency'] }))
}
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="once">One-time</option>
<option value="daily">{t('chores.daily')}</option>
<option value="weekly">{t('chores.weekly')}</option>
<option value="once">{t('chores.oneTime')}</option>
</select>
</div>

<div className="form-field">
<label className="form-label">Assign To</label>
<label className="form-label">{t('chores.assignTo')}</label>
<div className="chores-assign-grid">
{members.map((m: FamilyMember) => (
<button
Expand Down Expand Up @@ -490,20 +493,20 @@ export function ChoresView() {
className="btn btn--danger"
onClick={() => handleDeleteChore(editingChoreId)}
>
Delete
{t('common.delete')}
</button>
)}
<div className="modal-footer-right">
<button type="button" className="btn btn--secondary" onClick={handleCloseForm}>
Cancel
{t('common.cancel')}
</button>
<button
type="button"
className="btn btn--primary"
onClick={handleSaveChore}
disabled={!newChore.name.trim()}
>
{isEditing ? 'Save Changes' : 'Add Chore'}
{isEditing ? t('chores.saveChanges') : t('chores.addChore')}
</button>
</div>
</div>
Expand Down
22 changes: 12 additions & 10 deletions beacon/src/components/GroceryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GroceryList } from '../types/grocery';
import { AnyListClient } from '../api/anylist';
import { callHaService, hasToken } from '../api/ha-rest';
import { useLocalTasks } from '../hooks/useLocalTasks';
import { useTranslation } from '../i18n';

interface TodoItem {
uid: string;
Expand Down Expand Up @@ -40,6 +41,7 @@ interface GroceryViewProps {
}

export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = [] }: GroceryViewProps) {
const { t } = useTranslation();
const [haLists, setHaLists] = useState<GroceryList[]>([]);
const [selectedListId, setSelectedListId] = useState<string>(defaultListId || '');
const [haItems, setHaItems] = useState<TodoItem[]>([]);
Expand Down Expand Up @@ -249,11 +251,11 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
onChange={(e) => setSelectedListId(e.target.value)}
>
{allLists.length === 0 && (
<option value="">No lists available</option>
<option value="">{t('grocery.noListsAvailable')}</option>
)}
{allLists.map(list => (
<option key={list.id} value={list.id}>
{list.name}{list.source === 'ha' ? ' (HA)' : ''}
{list.source === 'ha' ? t('grocery.haList', { name: list.name }) : list.name}
</option>
))}
</select>
Expand All @@ -267,7 +269,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
ref={inputRef}
type="text"
className="grocery-view-input"
placeholder="Add an item..."
placeholder={t('grocery.addItemPlaceholder')}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
disabled={submitting || !selectedListId}
Expand All @@ -276,7 +278,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
type="submit"
className="grocery-view-add-btn"
disabled={!inputValue.trim() || submitting}
aria-label="Add item"
aria-label={t('grocery.addItem')}
>
<Plus size={20} />
</button>
Expand All @@ -285,7 +287,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
{/* Items list */}
<div className="grocery-view-list">
{loading && items.length === 0 ? (
<div className="grocery-view-loading">Loading items...</div>
<div className="grocery-view-loading">{t('grocery.loadingItems')}</div>
) : items.length === 0 && !loading ? (
<div className="grocery-view-empty">
<div className="grocery-view-empty-icon">
Expand All @@ -296,15 +298,15 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
<circle cx="20" cy="40" r="2" fill="var(--border)" />
</svg>
</div>
<p>No items yet — add one above</p>
<p>{t('grocery.empty')}</p>
</div>
) : (
<>
{/* Unchecked items */}
{uncheckedItems.length > 0 && (
<div className="grocery-view-section">
<div className="grocery-view-section-header">
Items ({uncheckedItems.length})
{t('grocery.items', { count: uncheckedItems.length })}
</div>
{uncheckedItems.map(item => (
<div key={item.uid} className="grocery-view-item-row">
Expand All @@ -321,7 +323,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
type="button"
className="grocery-view-delete"
onClick={() => handleDelete(item)}
aria-label="Delete item"
aria-label={t('grocery.deleteItem')}
>
<Trash2 size={14} />
</button>
Expand All @@ -340,7 +342,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
onClick={() => setCompletedCollapsed(prev => !prev)}
>
{completedCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
Completed ({checkedItems.length})
{t('grocery.completed', { count: checkedItems.length })}
</button>
{!completedCollapsed && checkedItems.map(item => (
<div key={item.uid} className="grocery-view-item-row">
Expand All @@ -367,7 +369,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
type="button"
className="grocery-view-delete"
onClick={() => handleDelete(item)}
aria-label="Delete item"
aria-label={t('grocery.deleteItem')}
>
<Trash2 size={14} />
</button>
Expand Down
15 changes: 10 additions & 5 deletions beacon/src/components/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useFamily } from '../hooks/useFamily';
import { useSettings } from '../hooks/useSettings';
import { MemberEarnings, formatChoreValue } from '../types/family';
import { localDayKey } from '../api/date-keys';
import { useTranslation } from '../i18n';

interface LeaderboardProps {
open: boolean;
Expand Down Expand Up @@ -34,6 +35,7 @@ function getMonthRange(): [string, string] {
const RANK_DECORATIONS = ['\u{1F947}', '\u{1F948}', '\u{1F949}'];

export function Leaderboard({ open, onClose }: LeaderboardProps) {
const { t } = useTranslation();
const { members } = useFamily();
const { settings } = useSettings();
const { getEarningsForPeriod, getStreakForMember } = useChores();
Expand Down Expand Up @@ -61,7 +63,7 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) {
return (
<div className={`slide-panel slide-panel--right ${open ? 'slide-panel--open' : ''}`}>
<div className="slide-panel-header">
<h2 className="slide-panel-title">Leaderboard</h2>
<h2 className="slide-panel-title">{t('leaderboard.title')}</h2>
<button type="button" className="modal-close" onClick={onClose}>
{'\u00D7'}
</button>
Expand All @@ -74,20 +76,20 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) {
className={`lb-period-btn ${period === 'week' ? 'lb-period-btn--active' : ''}`}
onClick={() => setPeriod('week')}
>
This Week
{t('leaderboard.thisWeek')}
</button>
<button
type="button"
className={`lb-period-btn ${period === 'month' ? 'lb-period-btn--active' : ''}`}
onClick={() => setPeriod('month')}
>
This Month
{t('leaderboard.thisMonth')}
</button>
</div>

<div className="slide-panel-body">
{ranked.length === 0 && (
<div className="chores-empty">Add family members and chores to see the leaderboard.</div>
<div className="chores-empty">{t('leaderboard.empty')}</div>
)}

<div className="lb-list">
Expand All @@ -109,7 +111,10 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) {
<div className="lb-info">
<span className="lb-name">{entry.member.name}</span>
<span className="lb-stats">
{entry.earnings.chore_count} chore{entry.earnings.chore_count !== 1 ? 's' : ''}
{entry.earnings.chore_count === 1
? t('leaderboard.choreCompletedSingular', { count: entry.earnings.chore_count })
: t('leaderboard.choresCompleted', { count: entry.earnings.chore_count })
}
</span>
</div>
<StreakBadge streak={entry.streak} size="sm" />
Expand Down
Loading
Loading